分别编译

  文件是传统的(在文件系统里的)存储单位和传统的编译单位。也存在着一些不同的系统,它们并不按照程序员可以看到的一集文件的方式存储、编译和展现C++程序。当然,这里的讨论将集中关注更传统的使用文件的系统。

  将一个完整的程序放入一个文件里通常是不可能的。特别是,在典型情况下,标准库和操作系统的代码都不是以源程序形式提供的,不能作为用户程序的一部分。对于具有真实规模的程序,将用户自己的所有代码放入一个文件也很不实际、不方便。采取将程序组织为一些文件的方式,可以强调它的逻辑结构,帮助读者理解程序,并帮助编译器去强迫实施这种逻辑结构。如果编译的单位就是一个文件,那么,当该文件或者它所依赖的什么东西在某个时候做了修改(哪怕非常小),这个文件的整体就必须重新编译。即使对一个中等规模的程序,将它划分为一些适当大小的文件,所节约的重编译时间也可能非常可观。

  用户将一个源文件提交给编译器后,首先进行的是该文件的预处理,也就是说,完成宏处理(7.8节),并按照#include指令引进所有头文件(2.4.1节、9.2.1节)。预处理之后的结果被称为编译单位。这种编译单位才是编译器真正的工作对象,也是C++语言的规则所描述的对象。在本书中,如果需要区分程序员所看到的东西和编译器所看到的东西时,我就会分别说源文件和编译单位。

  为了使分别编译能够工作,程序员必须提供各种声明,为孤立地分析一个编译单位提供有关程序其他部分的类型信息。在一个由许多分别编译的部分组成的程序里,这些声明必须保持一致,就像在由一个编译单位组成的程序里,所有声明都必须一致一样。你的系统中可能有某些工具能帮助你保证这一点。特别是连接器能够检查出许多种类的不一致性。连接器是一个程序,它的工作就是将分别编译的部分约束在一起。连接器有时也被(有些混乱地)称做装载器(loader)。连接工作可以在程序开始运行之前完全做好。另一种方式是允许在程序开始运行后为其加入新代码(动态连接)。

  程序是由一些文件组成的,这种组织通常被称为程序的物理结构。将一个程序物理地划分为一些相互分离的文件的工作应该根据程序的逻辑结构进行。对依赖性问题的关注指导着我们由一些名字空间组合出程序,同样的关注也指导着由源文件构成程序的组合过程。当然,一个程序的逻辑结构和物理结构不必完全相同。例如,可以用若干个源程序文件存储某一个名字空间里的函数,或者将几个名字空间定义放入同一个文件里,或者把一个名字空间的定义散布在若干个文件里,这些方式都很有价值(8.2.4节)。

  在这里,我们将首先考虑一些有关连接的技术细节,而后再讨论将计算器程序(6.1节、8.2节)分割成多个文件的两种方式。

🔚